package scanner;


import compiler.Properties;

import parser.GrammarSymbols;
import util.Arquivo;

/**
 * Scanner class
 * @version 2010-september-04
 * @discipline Compiladores
 * @author Gustavo H P Carvalho
 * @email gustavohpcarvalho@ecomp.poli.br
 */
public class Scanner {

	// The file object that will be used to read the source code
	private Arquivo file;
	// The last char read from the source code
	private char currentChar;
	// The kind of the current token
	private int currentKind;
	// Buffer to append characters read from file
	private StringBuffer currentSpelling;
	// Current line and column in the source file
	private int line, column;
	
	/**
	 * Default constructor
	 */
	public Scanner() {
		this.file = new Arquivo(Properties.sourceCodeLocation);		
		this.line = 0;
		this.column = 0;
		this.currentChar = this.file.readChar();
	}
	
	/**
	 * Returns the next token
	 * @return
	 */ //TODO
	public Token getNextToken() throws LexicalException {
		
		// Initializes the string buffer
	    this.currentSpelling = new StringBuffer("");
		
	    // Ignores separators
	    while(isSeparator(this.currentChar)){
	    	this.scanSeparator();
	    }	
	   
	    // Clears the string buffer
	    this.currentSpelling = new StringBuffer("");
	    
	    // Scans the next token
	    this.currentKind = this.scanToken();
	    
	    // Creates and returns a token for the lexema identified 
		Token token = new Token(this.currentKind, this.currentSpelling.toString(), this.line, this.column);
		return token;
	}
	
	/**
	 * Returns if a character is a separator
	 * @param c
	 * @return
	 */
	private boolean isSeparator(char c) {
		
		if ( c == '!' || c == ' ' || c == '\n' || c == '\t' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Reads (and ignores) a separator
	 * @throws LexicalException
	 */ //TODO
	private void scanSeparator() throws LexicalException {
		
		// If it is a comment line
			// Gets next char
			// Reads characters while they are graphics or '\t'
			// A command line should finish with a \n
		
		if(this.currentChar == '!'){
			this.getNextChar();
	        while(this.isGraphic(this.currentChar) || this.currentChar == '\t'){
	        	this.getNextChar();
	        }
        }else{
        	this.getNextChar();
        }
	}
	
	/**
	 * Gets the next char
	 */
	private void getNextChar() {
		
		
		// Appends the current char to the string buffer
		this.currentSpelling.append(this.currentChar);
		// Reads the next one
		this.currentChar = this.file.readChar();
		// Increments the line and column
		this.incrementLineColumn();
		
	
	}	
	/**
	 * Increments line and column
	 */
	private void incrementLineColumn() {
		// If the char read is a '\n', increments the line variable and assigns 0 to the column
		if ( this.currentChar == '\n' ) {
			this.line++;
			this.column = 0;
		// If the char read is not a '\n' 
		} else {
			// If it is a '\t', increments the column by 4
			if ( this.currentChar == '\t' ) {
				this.column = this.column + 4;
			// If it is not a '\t', increments the column by 1
			} else {
				this.column++;
			}
		}
	}
	
	/**
	 * Returns if a char is a digit (between 0 and 9)
	 * @param c
	 * @return
	 */
	private boolean isDigit(char c) {
		if ( c >= '0' && c <= '9' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a letter (between a and z or between A and Z)
	 * @param c
	 * @return
	 */
	private boolean isLetter(char c) {
		if ( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a graphic (any ASCII visible character)
	 * @param c
	 * @return
	 */
	private boolean isGraphic(char c) {
		if ( c >= ' ' && c <= '~' ) {
			return true;
		} else {
			return false;
		}
	}
	
	
	/**
	 * Scans the next token
	 * Simulates the DFA that recognizes the language described by the lexical grammar
	 * @return
	 * @throws LexicalException
	 */ //TODO
	private int scanToken() throws LexicalException {
		// The initial automata state is 0
		// While loop to simulate the automata
		
		// Se o caracter atual for uma letra, então o token deve ser um identificador ou palavra-chave da linguagem, respectivamente
		// Excluem-se aqui a palavras chave que se iniciam com ponto, pois são verificadas no próximo passo
		if(isLetter(this.currentChar)){
			return scanIdOrToken();
		}
		
		// Se o caractere atual for um dígito, o token deve ser um "number" -> digit+(.digit+)?
		if(isDigit(this.currentChar)){
			return scanNumber();
		}
		
		// Verifica se o caractere "ponto" corresponde a uma das palavras-chave (.true. | .false.), ou apenas a ele mesmo.
		if(this.currentChar == '.'){
			return scanPoint();
		}
		
		
		// Verifica se o caractere corresponde a um dos símbolos utilizados pelos operadores relacionais, aritméticos
		// ou outros símbolos especiais aceitos pela linguagem
		if(isGraphic(this.currentChar)){
			return scanGraphic();
		}
		
		return GrammarSymbols.EOT;
	}
	
	private int scanIdOrToken()throws LexicalException{
		
		// Inicialmente o carcter atual é uma letra, então pode corresponder a um ID ou um Token
		
		// Vai percorrendo os caracteres enquanto eles forem dígito ou letra
		
		while(isDigit(this.currentChar) || isLetter(this.currentChar)){
			  getNextChar();
		}
		
		// Após verificar todas as letras e dígitos, é esperado um separator ou um caracter especial (um parêntese, por exemplo)
		// Caso nenhum desses dois tipos sejam encontrados, uma Lexical Exception ser´lançada
		
		if(!isSpecialChars() && !isSeparator(this.currentChar)){
			throw new LexicalException("Caractere inválido na definição do ID ou Token",this.currentChar,this.line, this.column);
		}
		
		String keyWord = this.currentSpelling.toString();
		keyWord = keyWord.toLowerCase();
		
		
		// Verifica se o currentSpelling corresponde a alguma palavra-chave da linguagem
		
		if(keyWord.equals("integer"))
			return GrammarSymbols.TIPO_INTEIRO;
		if(keyWord.equals("real"))
			return GrammarSymbols.TIPO_REAL;
		if(keyWord.equals("logical"))
			return GrammarSymbols.TIPO_LOGICO;
		if(keyWord.equals("print"))
			return GrammarSymbols.PRINT;
		if(keyWord.equals("function"))
			return GrammarSymbols.FUNCTION;
		if(keyWord.equals("subroutine"))
			return GrammarSymbols.SUBROUTINE;
		if(keyWord.equals("call"))
			return GrammarSymbols.CALL;
		if(keyWord.equals("do"))
			return GrammarSymbols.DO;
		if(keyWord.equals("then"))
			return GrammarSymbols.THEN;
		if(keyWord.equals("end"))
			return GrammarSymbols.END;
		if(keyWord.equals("continue"))
			return GrammarSymbols.CONTINUE;
		if(keyWord.equals("exit"))
			return GrammarSymbols.EXIT;
		if(keyWord.equals("if"))
			return GrammarSymbols.IF;
		if(keyWord.equals("else"))
			return GrammarSymbols.ELSE;
		if(keyWord.equals("while"))
			return GrammarSymbols.WHILE;
		if(keyWord.equals("eot"))
			return GrammarSymbols.EOT;
		if(keyWord.equals("program"))
			return GrammarSymbols.PROGRAMA;

		
		// Caso o currentSpelling não corresponda a uma palavra-chave, então o lexema corresponde a um identificador (ID)
	
		return GrammarSymbols.ID;
		
	}
	
	private int scanNumber() throws LexicalException{
		
		int kindToken = GrammarSymbols.NUMERO_INTEIRO;
		
		while(isDigit(this.currentChar)){
			
			getNextChar();
			
			if(this.currentChar == '.'){
				getNextChar();
				
				while(isDigit(this.currentChar)){
					getNextChar();
				}
				
				if(isSpecialChars() || isSeparator(this.currentChar)){
					kindToken = GrammarSymbols.NUMERO_REAL;
				}else{
					throw new LexicalException("Caractere inválido na definição de número real", this.currentChar, this.line, this.column);
				}
			}	
		}
		
		if(isSpecialChars() || isSeparator(this.currentChar)){
			kindToken = GrammarSymbols.NUMERO_INTEIRO;
		}else{
			throw new LexicalException("Caractere inválido na definição de número inteiro", this.currentChar, this.line, this.column);
		}
		
		return kindToken;
	}
	
	private int scanPoint() throws LexicalException{
		
		int kindToken;
		
		getNextChar();
		
		if(isSeparator(this.currentChar)){
			kindToken = GrammarSymbols.PONTO;
		}else{
			while(!isSeparator(this.currentChar)){
				getNextChar();
			}
		}
		
		String keyWord = currentSpelling.toString();
			
		if(keyWord.equals(".true.")){
			kindToken = GrammarSymbols.VERDADEIRO;
		}
		
		else {
			if(keyWord.equals(".false.")){
				kindToken = GrammarSymbols.FALSO;
			} else{
				throw new LexicalException("Caractere inválido na definição do ID ou token (.true. ou .false. esperados)", this.currentChar, this.line, this.column);
			}
		}

		return kindToken;
	}
	
	private int scanGraphic() throws LexicalException{
		
		
		switch(this.currentChar){
		
			case '+':
				getNextChar();
				return GrammarSymbols.SOMA;
			
			case '-':
				getNextChar();
				return GrammarSymbols.SUBTRACAO;
			
			case '*':
				getNextChar();
				return GrammarSymbols.MULTIPLICACAO;
			
			case '/':
				getNextChar();
				if(this.currentChar == '='){
					getNextChar();
					return GrammarSymbols.DIFERENTE;
				}else{
					return GrammarSymbols.DIVISAO;
				}
				
			case '<':
				getNextChar();
				if(this.currentChar == '='){
					getNextChar();
					return GrammarSymbols.MENOR_OU_IGUAL;
				}else{
					return GrammarSymbols.MENOR_QUE;
				}
			
			case '>':
				getNextChar();
				if(this.currentChar == '='){
					getNextChar();
					return GrammarSymbols.MAIOR_OU_IGUAL;
				}else{
					return GrammarSymbols.MAIOR_QUE;
				}
				
			case '=':
				getNextChar();
				if(this.currentChar == '='){
					getNextChar();
					return GrammarSymbols.IGUAL_A;
				}else{
					return GrammarSymbols.ATRIBUICAO;
				}
			
			case '(':
				getNextChar();
				return GrammarSymbols.PARENTESE_ESQ;
			
			case ')':
				getNextChar();
				return GrammarSymbols.PARENTESE_DIR;
				
			case '\n':
				getNextChar();
				return GrammarSymbols.EOL;
				
			case ',':
				getNextChar();
				return GrammarSymbols.VIRGULA;
			case '"':
				getNextChar();
				return GrammarSymbols.ASPAS;
				
			case ':':
				getNextChar();
				if(this.currentChar == ':'){
					getNextChar();
					return GrammarSymbols.DECLARACAO;
				}else{
					throw new LexicalException("Method scanGraphic", this.currentChar, this.line, this.column);
				}
				
			default:
				throw new LexicalException("Method scanGraphic", this.currentChar, this.line, this.column);
				
		}
	}
	
	private boolean isSpecialChars() {
		
		return
			this.currentChar == ';' ||
			this.currentChar == '(' ||
			this.currentChar == ')' ||
			this.currentChar == '{' ||
			this.currentChar == '}' ||
			this.currentChar == '=' ||
			this.currentChar == '*' ||
			this.currentChar == '/' ||
			this.currentChar == '+' ||
			this.currentChar == '-' ||
			this.currentChar == '=' ||
			this.currentChar == '!' ||
			this.currentChar == '<' ||
			this.currentChar == '>' ||
			this.currentChar == ',';
	}
}